UNPKG

sysrot-hub

Version:

CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026

828 lines (727 loc) 21.4 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { PrismaClient } from '@prisma/client'; import { getSession } from 'next-auth/react'; import { z } from 'zod'; const prisma = new PrismaClient(); // Validation schemas const createTaskSchema = z.object({ title: z.string().min(1, 'Task title is required').max(200, 'Title too long'), description: z.string().max(5000, 'Description too long').optional(), // Classification type: z.enum(['EPIC', 'STORY', 'TASK', 'BUG', 'IMPROVEMENT', 'RESEARCH', 'SPIKE', 'SUB_TASK']).default('TASK'), priority: z.enum(['LOWEST', 'LOW', 'MEDIUM', 'HIGH', 'HIGHEST']).default('MEDIUM'), // Assignment assigneeId: z.string().optional(), categoryId: z.string().optional(), labelIds: z.array(z.string()).optional(), // Planning sprintId: z.string().optional(), epicId: z.string().optional(), // Time tracking estimatedHours: z.number().min(0).optional(), storyPoints: z.number().min(0).optional(), // Dates startDate: z.string().datetime().optional(), dueDate: z.string().datetime().optional(), // Business fields businessValue: z.number().min(0).optional(), riskLevel: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).default('LOW'), // Technical fields environment: z.string().optional(), version: z.string().optional(), // Custom fields customFields: z.record(z.any()).optional() }); const updateTaskSchema = createTaskSchema.partial().extend({ status: z.enum(['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'TESTING', 'DONE', 'CANCELLED', 'BLOCKED']).optional(), resolution: z.enum(['FIXED', 'WONT_FIX', 'DUPLICATE', 'INCOMPLETE', 'CANNOT_REPRODUCE', 'DONE']).optional() }); const assignTaskSchema = z.object({ assigneeId: z.string(), reason: z.string().optional() }); const commentSchema = z.object({ content: z.string().min(1, 'Comment cannot be empty').max(2000, 'Comment too long'), isInternal: z.boolean().default(false), parentId: z.string().optional() }); async function hasTaskPermission(userId: string, projectId: string, action: string = 'read') { const project = await prisma.project.findUnique({ where: { id: projectId }, include: { members: { where: { userId, isActive: true } } } }); if (!project) return false; // Project owner has all permissions if (project.ownerId === userId) return true; // Check project membership const member = project.members[0]; if (!member) return false; switch (action) { case 'read': return true; case 'create': case 'update': return ['OWNER', 'ADMIN', 'MANAGER', 'DEVELOPER'].includes(member.role); case 'delete': return ['OWNER', 'ADMIN', 'MANAGER'].includes(member.role); case 'assign': return ['OWNER', 'ADMIN', 'MANAGER'].includes(member.role); default: return false; } } async function generateTaskKey(projectId: string): Promise<string> { const project = await prisma.project.findUnique({ where: { id: projectId }, select: { key: true } }); if (!project) throw new Error('Project not found'); // Get the next task number for this project const lastTask = await prisma.task.findFirst({ where: { projectId, key: { startsWith: project.key + '-' } }, orderBy: { key: 'desc' } }); let nextNumber = 1; if (lastTask) { const lastNumber = parseInt(lastTask.key.split('-')[1] || '0'); nextNumber = lastNumber + 1; } return `${project.key}-${nextNumber}`; } export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const session = await getSession({ req }); if (!session?.user) { return res.status(401).json({ error: 'Authentication required' }); } const { projectId, action } = req.query; if (!projectId || typeof projectId !== 'string') { return res.status(400).json({ error: 'Project ID is required' }); } const { method } = req; // Handle specific actions if (action === 'assign' && method === 'POST') { return await assignTask(req, res, projectId); } else if (action === 'comment' && method === 'POST') { return await addComment(req, res, projectId); } else if (action === 'watch' && method === 'POST') { return await watchTask(req, res, projectId); } switch (method) { case 'GET': return await getTasks(req, res, projectId); case 'POST': return await createTask(req, res, projectId); default: res.setHeader('Allow', ['GET', 'POST']); return res.status(405).end('Method Not Allowed'); } } catch (error) { console.error('Tasks API error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function getTasks(req: NextApiRequest, res: NextApiResponse, projectId: string) { const session = await getSession({ req }); const { status, type, priority, assigneeId, sprintId, categoryId, search, limit = '50', page = '1', sortBy = 'createdAt', sortOrder = 'desc', includeSubtasks = 'false' } = req.query; try { // Check permissions const hasAccess = await hasTaskPermission(session.user.id, projectId, 'read'); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const pageNum = parseInt(page as string); const limitNum = parseInt(limit as string); const offset = (pageNum - 1) * limitNum; // Build where clause const whereClause: any = { projectId }; if (includeSubtasks === 'false') { whereClause.type = { not: 'SUB_TASK' }; } if (status && status !== 'ALL') { whereClause.status = status; } if (type) { whereClause.type = type; } if (priority) { whereClause.priority = priority; } if (assigneeId) { whereClause.assigneeId = assigneeId === 'unassigned' ? null : assigneeId; } if (sprintId) { whereClause.sprintId = sprintId === 'backlog' ? null : sprintId; } if (categoryId) { whereClause.categoryId = categoryId; } if (search && typeof search === 'string') { whereClause.OR = [ { title: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, { key: { contains: search, mode: 'insensitive' } } ]; } // Get tasks with comprehensive data const tasks = await prisma.task.findMany({ where: whereClause, include: { assignee: { select: { id: true, name: true, email: true, image: true } }, reporter: { select: { id: true, name: true, email: true, image: true } }, category: true, labels: true, sprint: { select: { id: true, name: true, status: true, startDate: true, endDate: true } }, epic: { select: { id: true, key: true, title: true, status: true } }, subtasks: { select: { id: true, key: true, title: true, status: true, priority: true, assignee: { select: { id: true, name: true, image: true } } } }, _count: { select: { comments: true, attachments: true, watchers: true, subtasks: true } } }, orderBy: { [sortBy as string]: sortOrder }, skip: offset, take: limitNum }); // Get total count const total = await prisma.task.count({ where: whereClause }); res.status(200).json({ tasks, pagination: { page: pageNum, limit: limitNum, total, pages: Math.ceil(total / limitNum) } }); } catch (error) { console.error('Error fetching tasks:', error); res.status(500).json({ error: 'Failed to fetch tasks' }); } } async function createTask(req: NextApiRequest, res: NextApiResponse, projectId: string) { const session = await getSession({ req }); try { // Check permissions const hasAccess = await hasTaskPermission(session.user.id, projectId, 'create'); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const validatedData = createTaskSchema.parse(req.body); // Generate unique task key const taskKey = await generateTaskKey(projectId); // Validate assignee if provided if (validatedData.assigneeId) { const assignee = await prisma.projectMember.findFirst({ where: { projectId, userId: validatedData.assigneeId, isActive: true } }); if (!assignee) { return res.status(400).json({ error: 'Assignee is not a project member' }); } } // Validate category if provided if (validatedData.categoryId) { const category = await prisma.taskCategory.findFirst({ where: { id: validatedData.categoryId, projectId } }); if (!category) { return res.status(400).json({ error: 'Invalid category' }); } } // Validate epic if provided if (validatedData.epicId) { const epic = await prisma.task.findFirst({ where: { id: validatedData.epicId, projectId, type: 'EPIC' } }); if (!epic) { return res.status(400).json({ error: 'Invalid epic' }); } } // Create the task const task = await prisma.task.create({ data: { ...validatedData, key: taskKey, projectId, reporterId: session.user.id, startDate: validatedData.startDate ? new Date(validatedData.startDate) : undefined, dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : undefined }, include: { assignee: { select: { id: true, name: true, email: true, image: true } }, reporter: { select: { id: true, name: true, email: true, image: true } }, category: true, labels: true, sprint: true, epic: { select: { id: true, key: true, title: true } } } }); // Add labels if provided if (validatedData.labelIds && validatedData.labelIds.length > 0) { await prisma.task.update({ where: { id: task.id }, data: { labels: { connect: validatedData.labelIds.map(id => ({ id })) } } }); } // Add creator as watcher await prisma.taskWatcher.create({ data: { taskId: task.id, userId: session.user.id } }); // Add assignee as watcher if different from creator if (validatedData.assigneeId && validatedData.assigneeId !== session.user.id) { await prisma.taskWatcher.create({ data: { taskId: task.id, userId: validatedData.assigneeId } }); } // Log task creation activity await prisma.projectActivity.create({ data: { projectId, userId: session.user.id, action: 'task_created', entityType: 'task', entityId: task.id, description: `Task "${task.title}" (${task.key}) was created`, metadata: { taskId: task.id, taskKey: task.key, taskType: task.type } } }); // Log task history await prisma.taskHistory.create({ data: { taskId: task.id, changedById: session.user.id, field: 'created', newValue: 'Task created', description: `Task ${task.key} was created` } }); res.status(201).json({ task, message: 'Task created successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error creating task:', error); res.status(500).json({ error: 'Failed to create task' }); } } async function assignTask(req: NextApiRequest, res: NextApiResponse, projectId: string) { const session = await getSession({ req }); const { taskId } = req.query; if (!taskId || typeof taskId !== 'string') { return res.status(400).json({ error: 'Task ID is required' }); } try { // Check permissions const hasAccess = await hasTaskPermission(session.user.id, projectId, 'assign'); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const validatedData = assignTaskSchema.parse(req.body); // Verify task belongs to project const task = await prisma.task.findFirst({ where: { id: taskId, projectId }, include: { assignee: { select: { id: true, name: true } } } }); if (!task) { return res.status(404).json({ error: 'Task not found' }); } // Verify assignee is project member const assignee = await prisma.projectMember.findFirst({ where: { projectId, userId: validatedData.assigneeId, isActive: true }, include: { user: { select: { id: true, name: true, email: true, image: true } } } }); if (!assignee) { return res.status(400).json({ error: 'Assignee is not a project member' }); } const previousAssigneeId = task.assigneeId; // Update task assignment const updatedTask = await prisma.task.update({ where: { id: taskId }, data: { assigneeId: validatedData.assigneeId }, include: { assignee: { select: { id: true, name: true, email: true, image: true } } } }); // Add assignee as watcher await prisma.taskWatcher.upsert({ where: { taskId_userId: { taskId, userId: validatedData.assigneeId } }, update: {}, create: { taskId, userId: validatedData.assigneeId } }); // Log assignment change await prisma.taskHistory.create({ data: { taskId, changedById: session.user.id, field: 'assignee', oldValue: previousAssigneeId || 'Unassigned', newValue: validatedData.assigneeId, description: `Task assigned to ${assignee.user.name}` } }); // Log activity await prisma.projectActivity.create({ data: { projectId, userId: session.user.id, action: 'task_assigned', entityType: 'task', entityId: taskId, description: `Task ${task.key} assigned to ${assignee.user.name}`, metadata: { taskId, taskKey: task.key, assigneeId: validatedData.assigneeId, assigneeName: assignee.user.name, reason: validatedData.reason } } }); res.status(200).json({ task: updatedTask, message: 'Task assigned successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error assigning task:', error); res.status(500).json({ error: 'Failed to assign task' }); } } async function addComment(req: NextApiRequest, res: NextApiResponse, projectId: string) { const session = await getSession({ req }); const { taskId } = req.query; if (!taskId || typeof taskId !== 'string') { return res.status(400).json({ error: 'Task ID is required' }); } try { // Check permissions const hasAccess = await hasTaskPermission(session.user.id, projectId, 'read'); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } const validatedData = commentSchema.parse(req.body); // Verify task exists const task = await prisma.task.findFirst({ where: { id: taskId, projectId } }); if (!task) { return res.status(404).json({ error: 'Task not found' }); } // Verify parent comment if specified if (validatedData.parentId) { const parentComment = await prisma.taskComment.findFirst({ where: { id: validatedData.parentId, taskId } }); if (!parentComment) { return res.status(400).json({ error: 'Parent comment not found' }); } } // Create comment const comment = await prisma.taskComment.create({ data: { taskId, authorId: session.user.id, content: validatedData.content, isInternal: validatedData.isInternal, parentId: validatedData.parentId }, include: { author: { select: { id: true, name: true, email: true, image: true } }, replies: { include: { author: { select: { id: true, name: true, image: true } } } } } }); // Add commenter as watcher await prisma.taskWatcher.upsert({ where: { taskId_userId: { taskId, userId: session.user.id } }, update: {}, create: { taskId, userId: session.user.id } }); // Log activity await prisma.projectActivity.create({ data: { projectId, userId: session.user.id, action: 'comment_added', entityType: 'task', entityId: taskId, description: `Comment added to task ${task.key}`, metadata: { taskId, taskKey: task.key, commentId: comment.id, isInternal: validatedData.isInternal } } }); res.status(201).json({ comment, message: 'Comment added successfully' }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation failed', details: error.errors }); } console.error('Error adding comment:', error); res.status(500).json({ error: 'Failed to add comment' }); } } async function watchTask(req: NextApiRequest, res: NextApiResponse, projectId: string) { const session = await getSession({ req }); const { taskId, action: watchAction } = req.query; if (!taskId || typeof taskId !== 'string') { return res.status(400).json({ error: 'Task ID is required' }); } try { // Check permissions const hasAccess = await hasTaskPermission(session.user.id, projectId, 'read'); if (!hasAccess) { return res.status(403).json({ error: 'Access denied' }); } // Verify task exists const task = await prisma.task.findFirst({ where: { id: taskId, projectId } }); if (!task) { return res.status(404).json({ error: 'Task not found' }); } if (watchAction === 'unwatch') { // Remove watcher await prisma.taskWatcher.deleteMany({ where: { taskId, userId: session.user.id } }); res.status(200).json({ message: 'Successfully unwatched task' }); } else { // Add watcher await prisma.taskWatcher.upsert({ where: { taskId_userId: { taskId, userId: session.user.id } }, update: {}, create: { taskId, userId: session.user.id } }); res.status(200).json({ message: 'Successfully watching task' }); } } catch (error) { console.error('Error watching task:', error); res.status(500).json({ error: 'Failed to watch task' }); } }